package com.hzw.saas.service.storage.service;

import cn.hutool.core.codec.Base62;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.text.StrSplitter;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.service.IService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hzw.saas.api.storage.*;
import com.hzw.saas.api.storage.bo.*;
import com.hzw.saas.api.storage.enums.FileIntegrityEnum;
import com.hzw.saas.api.storage.enums.ResourceStatusEnum;
import com.hzw.saas.api.user.IUserAccountService;
import com.hzw.saas.api.user.IUserProfileService;
import com.hzw.saas.api.user.bo.UserAccountBO;
import com.hzw.saas.api.user.bo.UserBaseInfoBO;
import com.hzw.saas.api.user.bo.UserProfileBO;
import com.hzw.saas.common.config.exception.DefaultSaasException;
import com.hzw.saas.common.config.query.LogicQuery;
import com.hzw.saas.common.config.util.AssertUtil;
import com.hzw.saas.common.config.util.RedisUtil;
import com.hzw.saas.common.security.utils.SecurityUtils;
import com.hzw.saas.common.util.MapperUtil;
import com.hzw.saas.common.util.SnowFlakeUtil;
import com.hzw.saas.common.util.StringUtils;
import com.hzw.saas.common.util.constants.CacheConsts;
import com.hzw.saas.common.util.constants.PathConsts;
import com.hzw.saas.common.util.enums.AssertEnum;
import com.hzw.saas.common.util.enums.ConflictModeEnum;
import com.hzw.saas.service.storage.mapper.ResourceShareMapper;
import com.hzw.saas.service.storage.model.ResourceShare;
import com.hzw.saas.service.storage.util.StorageUtil;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @author zzl
 * @since 07/02/2021
 */
@Service
@RequiredArgsConstructor
@Slf4j
public class ResourceShareServiceImpl extends ServiceImpl<ResourceShareMapper, ResourceShare> implements IResourceShareService, IService<ResourceShare> {

    private static final String CACHE_GROUP = "shareAuth" + CacheConsts.CACHE_KEY_JOINER;
    private static final Long CACHE_EXPIRE_IN = 60 * 60L;

    private final IStorageService storageService;
    private final IResourceService resourceService;
    private final IResourceFolderService folderService;
    private final IResourceFileService fileService;

    private final IUserAccountService userAccountService;
    private final IUserProfileService userProfileService;

    @Override
    public UserBaseInfoBO getUserInfo(String shareId) {
        ResourceShare shareDO = this.getShareDO(shareId);
        UserBaseInfoBO user = UserBaseInfoBO.emptyInfo();
        if (Objects.isNull(shareDO))
            return user;
        Optional<UserAccountBO> account = Optional.ofNullable(userAccountService.getAccount(shareDO.getCreatorId()));
        Optional<UserProfileBO> profile = Optional.ofNullable(userProfileService.get(shareDO.getCreatorId()));
        user.setUserId(shareDO.getCreatorId());
        user.setUserName(account.map(UserAccountBO::getUserName).orElse(""));
        user.setAvatar(profile.map(UserProfileBO::getAvatar).orElse(""));
        return user;
    }

    @Override
    public ResourceShareTokenBO checkSharePwd(String shareId, String password) {
        ResourceShare shareDO = this.getShareDO(shareId);
        this.assertAuthRead(shareDO);
        if (StrUtil.isBlank(shareDO.getPassword())) {
            return new ResourceShareTokenBO("", -1L);
        }
        if (Objects.equals(shareDO.getPassword(), password)) {
            return this.encodeAccessToken(shareId);
        }
        throw DefaultSaasException.build("密码错误").status(HttpStatus.BAD_REQUEST);
    }

    @Override
    public void assertValidToken(String shareId, String accessToken) {
        ResourceShare shareDO = this.getShareDO(shareId);
        this.assertAuthRead(shareDO);
        if (StrUtil.isBlank(shareDO.getPassword()))
            return;
        String tokenShareId = this.decodeAccessToken(accessToken);
        AssertUtil.assertThrow("未授权", HttpStatus.FORBIDDEN, !Objects.equals(shareId, tokenShareId));
    }

    @Override
    public void assertAuthOwn(String userId, String shareId) {
        int count = super.lambdaQuery()
            .eq(ResourceShare::getPid, shareId)
            .eq(ResourceShare::getCreatorId, userId)
            .count();
        AssertUtil.assertThrow("无此分享权限", HttpStatus.BAD_REQUEST, count == 0);
    }

    @Override
    public void assertAuthOwn(String userId, List<String> shareIds) {
        if (CollectionUtil.isEmpty(shareIds))
            return;
        for (String shareId : shareIds) {
            this.assertAuthOwn(userId, shareId);
        }
    }

    @Override
    public ResourceSharePreviewBO listSharedResources(String shareId, String supIdPath) {
        ResourceShare shareDO = this.getShareDO(shareId);
        this.assertAuthRead(shareDO, supIdPath);
        return this.listSharedResources(shareDO, supIdPath);
    }

    @Override
    public List<ResourceShareBO> listShare(String userId, String spaceId) {
        if (StrUtil.isBlank(userId))
            return new ArrayList<>();
        List<ResourceShare> list = super.lambdaQuery()
            .eq(ResourceShare::getCreatorId, userId)
            .eq(StrUtil.isNotBlank(spaceId), ResourceShare::getSpaceId, spaceId)
            .orderByDesc(ResourceShare::getCreateTime)
            .list();
        return convert(list);
    }

    @Override
    public String share(String userId, String spaceId, String shareName, List<String> folderIdList, List<String> fileIdList, String password, Date expireTime) {
        boolean allEmpty = CollectionUtil.isEmpty(folderIdList) && CollectionUtil.isEmpty(fileIdList);
        AssertUtil.assertThrow("必须至少选择一个文件或目录", allEmpty);
        if (CollectionUtil.isNotEmpty(folderIdList)) {
            folderIdList = StringUtils.trimAndDistinct(folderIdList);
        }
        if (CollectionUtil.isNotEmpty(fileIdList)) {
            fileIdList = StringUtils.trimAndDistinct(fileIdList);
        }
        ShareInfo shareInfo = new ShareInfo(folderIdList, fileIdList);
        ResourceShare entity2Save = new ResourceShare();
        entity2Save.setPid(SnowFlakeUtil.nextIdStr());
        entity2Save.setCreatorId(userId);
        entity2Save.setSpaceId(spaceId);
        entity2Save.setPassword(StrUtil.isBlank(password) ? StrUtil.EMPTY : password);
        entity2Save.setExpireTime(expireTime);
        if (StrUtil.isNotBlank(shareName))
            entity2Save.setShareName(shareName);
        entity2Save.setShareInfo(encodeShareInfo(shareInfo));
        super.save(entity2Save);
        return entity2Save.getPid();
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void saveSharedResources(String shareId, String tarSupId, List<String> folderIdPaths, List<String> fileIdPaths) {
        boolean allEmpty = CollectionUtil.isEmpty(folderIdPaths) && CollectionUtil.isEmpty(fileIdPaths);
        AssertUtil.assertThrow("必须至少选择一个文件或目录", allEmpty);
        ResourceShare shareDO = this.getShareDO(shareId);
        this.assertAuthRead(shareDO, folderIdPaths, fileIdPaths);
        List<String> folderIds = folderIdPaths.stream().map(folderIdPath -> {
            List<String> ids = this.splitPath(folderIdPath);
            return ids.isEmpty() ? "" : ids.get(ids.size() - 1);
        }).collect(Collectors.toList());
        List<String> fileIds = fileIdPaths.stream().map(fileIdPath -> {
            List<String> ids = this.splitPath(fileIdPath);
            return ids.isEmpty() ? "" : ids.get(ids.size() - 1);
        }).collect(Collectors.toList());
        String userId = SecurityUtils.getUser().getUserId();
        String spaceId = this.getSpaceId(tarSupId);
        storageService.assertNotOverSize(userId, spaceId, fileIds, folderIds);

        fileService.copyFiles(fileIds, tarSupId, ConflictModeEnum.THROW_ERR);
        folderService.copyFolders(folderIds, tarSupId, ConflictModeEnum.THROW_ERR);
    }

    @Override
    public boolean updateShare(String shareId, String password, Date expireTime) {
        if (StrUtil.isBlank(shareId))
            return false;
        if (StrUtil.isNotBlank(password)) {
            this.deleteShareToken(shareId);
        }
        return super.lambdaUpdate().eq(ResourceShare::getPid, shareId)
            .set(Objects.nonNull(password), ResourceShare::getPassword, password)
            .set(Objects.nonNull(expireTime), ResourceShare::getExpireTime, expireTime).update();
    }

    @Override
    public boolean deleteShare(String shareId) {
        if (StrUtil.isBlank(shareId))
            return false;
        this.deleteShareToken(shareId);
        return super.removeById(shareId);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean deleteShare(List<String> shareIdList) {
        if (CollectionUtil.isEmpty(shareIdList))
            return true;
        shareIdList = StringUtils.trimAndDistinct(shareIdList);
        shareIdList.forEach(this::deleteShareToken);
        return super.lambdaUpdate().in(ResourceShare::getPid, shareIdList).remove();
    }

    private ResourceShare getShareDO(String shareId) {
        return super.getById(shareId);
    }

    private void assertAuthRead(ResourceShare share) {
        AssertUtil.assertThrow("分享未找到或已被删除", HttpStatus.NOT_FOUND, Objects.isNull(share));
        boolean dateMatched = Objects.isNull(share.getExpireTime()) || share.getExpireTime().after(new Date());
        AssertUtil.assertThrow("分享已过期", HttpStatus.NOT_FOUND, !dateMatched);
    }

    private void assertAuthRead(ResourceShare share, String supIdPath) {
        this.assertAuthRead(share);
        boolean checkPath = this.checkPath(share, supIdPath);
        AssertUtil.assertThrow("资源未找到或无权限", HttpStatus.BAD_REQUEST, !checkPath);
    }

    private void assertAuthRead(ResourceShare share, List<String> folderIdPaths, List<String> fileIdPaths) {
        this.assertAuthRead(share);
        boolean checkPath = this.checkPath(share, folderIdPaths, fileIdPaths);
        AssertUtil.assertThrow("资源未找到或无权限", HttpStatus.BAD_REQUEST, !checkPath);
    }

    private boolean checkPath(ResourceShare share, String supIdPath) {
        if (StrUtil.isBlank(supIdPath))
            return true;
        List<String> ids = this.splitPath(supIdPath);
        if (ids.isEmpty())
            return true;
        ShareInfo shareInfo = this.decodeShareInfo(share.getShareInfo());
        if (CollectionUtil.isEmpty(shareInfo.getFolderIds()))
            return false;
        if (!shareInfo.getFolderIds().contains(ids.get(0)))
            return false;
        return this.checkPath(ids);
    }

    private boolean checkPath(ResourceShare share, List<String> folderIdPaths, List<String> fileIdPaths) {
        ShareInfo shareInfo = this.decodeShareInfo(share.getShareInfo());
        Map<String, List<String>> folderMap = new HashMap<>();
        Map<String, List<String>> fileMap = new HashMap<>();

        if (CollectionUtil.isNotEmpty(folderIdPaths)) {
            if (CollectionUtil.isEmpty(shareInfo.getFolderIds()))
                return false;
            for (String folderIdPath : folderIdPaths) {
                List<String> supIds = this.splitPath(folderIdPath);
                if (supIds.isEmpty()) {
                    continue;
                }
                String folderId = supIds.remove(supIds.size() - 1);
                String supIdPath = String.join(PathConsts.SLASH, supIds);
                String rootId = supIds.isEmpty() ? folderId : supIds.get(0);
                // 检查根目录是否在分享列表中
                if (!shareInfo.getFolderIds().contains(rootId)) {
                    return false;
                }
                // 如果该目录就为根目录
                if (supIds.size() == 0) {
                    continue;
                }
                // 检查缓存
                List<String> subIds = folderMap.get(supIdPath);
                // 添加缓存
                if (Objects.isNull(subIds)) {
                    // 检查父路径
                    if (!this.checkPath(supIds)) {
                        return false;
                    }
                    // 获取子目录列表
                    subIds = folderService.listSubFolderPids(supIds.get(supIds.size() - 1),
                        LogicQuery.eq(ResourceStatusEnum.NORMAL.getCode()));
                    // 添加缓存
                    folderMap.put(supIdPath, subIds);
                }
                if (!subIds.contains(folderId)) {
                    return false;
                }
            }
        }

        if (CollectionUtil.isNotEmpty(fileIdPaths)) {
            for (String fileIdPath : fileIdPaths) {
                List<String> supIds = this.splitPath(fileIdPath);
                if (supIds.isEmpty()) {
                    continue;
                }
                String fileId = supIds.remove(supIds.size() - 1);
                String supIdPath = String.join(PathConsts.SLASH, supIds);
                // 检查文件自身是否在分享列表中
                if (supIds.isEmpty()) {
                    if (!shareInfo.getFileIds().contains(fileId)) {
                        return false;
                    }
                    continue;
                }
                // 检查根目录是否在分享列表中
                if (!shareInfo.getFolderIds().contains(supIds.get(0))) {
                    return false;
                }
                // 检查缓存
                List<String> subIds = fileMap.get(supIdPath);
                // 添加缓存
                if (Objects.isNull(subIds)) {
                    if (!this.checkPath(supIds)) {
                        return false;
                    }
                    subIds = fileService.listSubFilePids(supIds.get(supIds.size() - 1),
                        LogicQuery.eq(ResourceStatusEnum.NORMAL.getCode()),
                        LogicQuery.eq(FileIntegrityEnum.NORMAL.getCode()));
                    fileMap.put(supIdPath, subIds);
                }
                if (!subIds.contains(fileId)) {
                    return false;
                }
            }
        }
        return true;
    }

    private boolean checkPath(List<String> folderIds) {
        return this.folderService.isValidPidPath(folderIds);
    }

    private ResourceSharePreviewBO listSharedResources(ResourceShare share, String supIdPath) {
        List<String> ids = this.splitPath(supIdPath);
        ResourceSharePreviewBO previewBO = MapperUtil.nf().map(share, ResourceSharePreviewBO.class);
        if (ids.isEmpty()) {
            previewBO.setSupIdPath("/");
            ShareInfo shareInfo = this.decodeShareInfo(share.getShareInfo());
            List<ResourceFolderStructBO> folders = folderService.listFolders(shareInfo.getFolderIds());
            List<ResourceFileBO> files = fileService.listFiles(shareInfo.getFileIds());
            previewBO.setFolders(folders);
            previewBO.setFiles(files);
            return previewBO;
        } else {
            previewBO.setSupIdPath(supIdPath);
            ResourcesBO resourcesBO = resourceService.listSubResources(ids.get(ids.size() - 1),
                LogicQuery.eq(ResourceStatusEnum.NORMAL.getCode()), 1, true);
            previewBO.setFolders(resourcesBO.getFolders());
            previewBO.setFiles(resourcesBO.getFiles());
        }
        return previewBO;
    }

    private List<ResourceShareBO> convert(List<ResourceShare> shareDOList) {
        List<ResourceShareBO> result = new ArrayList<>();
        if (CollectionUtil.isNotEmpty(shareDOList)) {
            for (ResourceShare shareDO : shareDOList) {
                ResourceShareBO shareBO = MapperUtil.nf().map(shareDO, ResourceShareBO.class);
                ShareInfo shareInfo = decodeShareInfo(shareDO.getShareInfo());
                List<ResourceFolderStructBO> folderList = folderService.listFolders(shareInfo.getFolderIds());
                List<ResourceFileBO> fileList = fileService.listFiles(shareInfo.getFileIds());
                shareBO.setFolders(folderList);
                shareBO.setFiles(fileList);
                result.add(shareBO);
            }
        }
        return result;
    }

    private String encodeShareInfo(ShareInfo shareInfo) {
        return Objects.nonNull(shareInfo) ? Base64.encode(JSONUtil.toJsonStr(shareInfo)) : null;
    }

    private ShareInfo decodeShareInfo(String shareInfo) {
        return StrUtil.isNotBlank(shareInfo) ? JSONUtil.toBean(Base64.decodeStr(shareInfo), ShareInfo.class) : new ShareInfo();
    }

    private ResourceShareTokenBO encodeAccessToken(String shareId) {
        String key = CACHE_GROUP.concat(shareId).concat(CacheConsts.CACHE_KEY_JOINER).concat(SnowFlakeUtil.nextIdStr());
        RedisUtil.set(key, shareId, CACHE_EXPIRE_IN);
        return new ResourceShareTokenBO(Base62.encode(key, StandardCharsets.UTF_8), CACHE_EXPIRE_IN);
    }

    private String decodeAccessToken(String accessToken) {
        if (StrUtil.isBlank(accessToken))
            return null;
        String key = Base62.decodeStr(accessToken, StandardCharsets.UTF_8);
        Object o = RedisUtil.get(key);
        if (Objects.nonNull(o)) {
            RedisUtil.expire(key, CACHE_EXPIRE_IN);
            return String.valueOf(o);
        }
        return null;
    }

    private void deleteShareToken(String shareId) {
        String keyPattern = CACHE_GROUP.concat(shareId).concat(CacheConsts.CACHE_KEY_JOINER).concat(CacheConsts.CACHE_KEY_WILDCARD);
        RedisUtil.delByPattern(keyPattern);
    }

    private List<String> splitPath(String path) {
        return StrSplitter.splitByRegex(path, PathConsts.MORE_SLASH_SPACE_PATTERN, -1, true, true);
    }

    /**
     * 根据目录ID获取存储空间ID
     *
     * @param folderId
     * @return
     */
    private String getSpaceId(String folderId) {
        if (StorageUtil.isSpaceId(folderId))
            return folderId;
        if (StorageUtil.isRootId(folderId))
            return StorageUtil.parseSpaceIdByRootId(folderId);
        ResourceFolderStructBO folder = folderService.getFolder(folderId, AssertEnum.FOUND);
        return folder.getSpaceId();
    }

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    private static class ShareInfo {
        private List<String> folderIds = new ArrayList<>();
        private List<String> fileIds = new ArrayList<>();
    }

}
